/*
 * Copyright 2007 Ulf Ochsenfahrt <ulf@ofahrt.de>
 *
 * This file is part of mnemoj, a software transactional memory implementation
 * that is based on Project Darkstar.
 *
 * mnemoj is free software: you can redistribute it
 * and/or modify it under the terms of the GNU General Public License
 * version 2 as published by the Free Software Foundation and
 * distributed hereunder to you.
 *
 * Project Darkstar Server is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package de.ofahrt.mnemoj.impl;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicLong;

import SQLite.Callback;
import SQLite.Database;
import SQLite.Stmt;

import com.sun.sgs.app.ManagedObject;
import com.sun.sgs.app.NameNotBoundException;
import com.sun.sgs.app.ObjectNotFoundException;
import com.sun.sgs.app.Task;
import com.sun.sgs.app.TransactionException;

public final class SqliteStorage implements Storage
{

	private static final class EmptyCallback implements Callback
	{
		private EmptyCallback()
		{/*OK*/}
		public void columns(String[] arg0)
		{/*OK*/}
		public boolean newrow(String[] arg0)
		{ return false; }
		public void types(String[] arg0)
		{/*OK*/}
	}

private final Database db;
private final Callback callback;

private final Stmt getEntryStmt;
private final Stmt createEntryStmt;
private final Stmt updateEntryStmt;
private final Stmt deleteEntryStmt;

private final Stmt getBindingStmt;
private final Stmt createBindingStmt;
private final Stmt updateBindingStmt;
private final Stmt deleteBindingStmt;

private final Object databaseLock = new Object();
private final Object cacheLock = new Object();
//private final Object timeLock = new Object();
private final HashMap<Long,ManagedEntry> entryCache = new HashMap<Long,ManagedEntry>();
private final HashMap<String,Binding> bindingCache = new HashMap<String,Binding>();
private final HashSet<ManagedTask> taskCache = new HashSet<ManagedTask>();
private final AtomicLong entryIds;
private final AtomicLong currentTaskId;

public SqliteStorage(File f)
{
	try
	{
		db = new Database();
		db.open(f.getPath(), 0666);
		callback = new EmptyCallback();
		db.exec("create table if not exists entries (id INTEGER PRIMARY KEY, data BLOB)", callback);
		db.exec("create table if not exists bindings (id STRING PRIMARY KEY, data INTEGER)", callback);
		db.exec("create table if not exists tasks (id INTEGER PRIMARY KEY, time INTEGER, link INTEGER, code BLOB)", callback);
//		db.exec("create index if not exists on tasks time", callback);
		
		getEntryStmt = db.prepare("SELECT data FROM entries WHERE id=?");
		createEntryStmt = db.prepare("INSERT INTO entries VALUES (?, ?)");
		updateEntryStmt = db.prepare("UPDATE entries SET data=? WHERE id=?");
		deleteEntryStmt = db.prepare("DELETE FROM entries WHERE id=?");
		
		getBindingStmt = db.prepare("SELECT data FROM bindings WHERE id=?");
		createBindingStmt = db.prepare("INSERT INTO bindings VALUES (?, ?)");
		updateBindingStmt = db.prepare("UPDATE bindings SET data=? WHERE id=?");
		deleteBindingStmt = db.prepare("DELETE FROM bindings WHERE id=?");
		
		{
			Stmt stmt = db.prepare("SELECT max(id) from entries");
			if (stmt.step())
			{
				entryIds = new AtomicLong(stmt.column_long(0));
				while (stmt.step()) {/*Do nothing.*/}
			}
			else
				entryIds = new AtomicLong();
		}
		{
			Stmt stmt = db.prepare("SELECT max(id) from tasks");
			if (stmt.step())
			{
				currentTaskId = new AtomicLong(stmt.column_long(0));
				while (stmt.step()) {/*Do nothing.*/}
			}
			else
				currentTaskId = new AtomicLong();
		}
	}
	catch (Exception e)
	{ throw new RuntimeException(e); }
}

private ManagedEntry readEntry(Long id)
{
	synchronized (cacheLock)
	{
		synchronized (databaseLock)
		{
			try
			{
				getEntryStmt.reset();
				getEntryStmt.clear_bindings();
				getEntryStmt.bind(1, id.longValue());
				if (!getEntryStmt.step()) throw new ObjectNotFoundException();
				byte[] b = getEntryStmt.column_bytes(0);
				while (getEntryStmt.step()) {/*Do nothing*/}
				ManagedEntry result = new ManagedEntry(id);
				result.set(b);
				entryCache.put(id, result);
				return result;
			}
			catch (Exception e)
			{ throw new TransactionException(e.getMessage()); }
		}
	}
}

private Binding readBinding(String id)
{
	synchronized (cacheLock)
	{
		synchronized (databaseLock)
		{
			try
			{
				getBindingStmt.reset();
				getBindingStmt.clear_bindings();
				getBindingStmt.bind(1, id);
				if (!getBindingStmt.step()) return null;
				Long targetid = Long.valueOf(getBindingStmt.column_long(0));
				Binding result = new Binding(id, new ManagedReferenceImpl(targetid));
				bindingCache.put(id, result);
				return result;
			}
			catch (Exception e)
			{ throw new TransactionException(e); }
		}
	}
}

public ManagedEntry getEntry(Long id)
{
	ManagedEntry entry;
	synchronized (cacheLock)
	{
		entry = entryCache.get(id);
		if (entry == null) entry = readEntry(id);
		if (entry == null) throw new ObjectNotFoundException();
	}
	entry.getLock().lock();
	return entry;
}

public ManagedEntry createEntry()
{
	Long id = Long.valueOf(entryIds.incrementAndGet());
	ManagedEntry entry = new ManagedEntry(id);
	entry.getLock().lock();
	return entry;
}

private Binding obtainBinding(String id, boolean create)
{
	Binding binding;
	synchronized (cacheLock)
	{
		binding = bindingCache.get(id);
		if (binding == null) binding = readBinding(id);
		if (binding == null)
		{
			if (create)
			{
				binding = new Binding(id);
				Binding b = bindingCache.put(id, binding);
				if (b != null) binding = b;
			}
			else
				throw new NameNotBoundException(id);
		}
	}
	binding.getLock().lock();
	return binding;
}

public Binding getBinding(String id)
{ return obtainBinding(id, false); }

public Binding createBinding(String id)
{ return obtainBinding(id, true); }

private void resetStmt(Stmt stmt) throws Exception
{
	stmt.reset();
	stmt.clear_bindings();
}

private void applyStmt(Stmt stmt) throws Exception
{
	while (stmt.step()) {/*Do nothing*/}
}

public void commit(Transaction transaction)
{
	long time = getCurrentTime();
	
	IdentityHashMap<ManagedEntry,byte[]> convertedEntries = new IdentityHashMap<ManagedEntry,byte[]>();
	for (Entry<ManagedEntry,ManagedObject> e : transaction.instancedEntries.entrySet())
	{
		ManagedEntry entry = e.getKey();
		ManagedObject value = e.getValue();
		if (value != null)
			convertedEntries.put(entry, SerializationHelper.serialize(value));
	}
	
	ArrayList<ManagedTask> convertedTasks = new ArrayList<ManagedTask>();
	for (TaskEntry e : transaction.tasks)
	{
		long attime = time+e.getDelay();
		long id = currentTaskId.incrementAndGet();
		while (id >= Long.MAX_VALUE/2)
		{
			currentTaskId.compareAndSet(id, 0);
			id = currentTaskId.incrementAndGet();
		}
		Task task = e.getTask();
		if (task instanceof ManagedObject)
		{
			ManagedObject mo = (ManagedObject) task;
			convertedTasks.add(new ManagedTask(attime, id, transaction.knownObjects.get(mo)));
		}
		else
			convertedTasks.add(new ManagedTask(attime, id, SerializationHelper.serialize(task)));
	}
	
	// no exception may happen after this point
	// starting at this point, the in-memory cache structures are manipulated
	synchronized (cacheLock)
	{
		synchronized (databaseLock)
		{
			try
			{
				db.exec("BEGIN", callback);
				for (Entry<ManagedEntry,ManagedObject> e : transaction.instancedEntries.entrySet())
				{
					ManagedEntry entry = e.getKey();
					ManagedObject value = e.getValue();
					if (value == null)
					{
						resetStmt(deleteEntryStmt);
						deleteEntryStmt.bind(1, entry.getId().longValue());
						applyStmt(deleteEntryStmt);
						entryCache.remove(entry.getId());
						entry.set(null);
					}
					else
					{
						if (entry.isNew())
						{
							resetStmt(createEntryStmt);
							createEntryStmt.bind(1, entry.getId().longValue());
							createEntryStmt.bind(2, convertedEntries.get(entry));
							applyStmt(createEntryStmt);
							entryCache.put(entry.getId(), entry);
							entry.set(convertedEntries.get(entry));
						}
						else
						{
							resetStmt(updateEntryStmt);
							updateEntryStmt.bind(2, entry.getId().longValue());
							updateEntryStmt.bind(1, convertedEntries.get(entry));
							applyStmt(updateEntryStmt);
							entry.set(convertedEntries.get(entry));
						}
					}
					entry.getLock().unlock();
				}
				
				IdentityHashMap<Binding,ManagedReferenceImpl> lockedBindings = transaction.instancedBindings;
				for (Entry<Binding,ManagedReferenceImpl> e : lockedBindings.entrySet())
				{
					Binding binding = e.getKey();
					ManagedReferenceImpl ref = e.getValue();
					if (ref == null)
					{
						resetStmt(deleteBindingStmt);
						deleteBindingStmt.bind(1, binding.getId());
						applyStmt(deleteBindingStmt);
					}
					else
					{
						if (binding.isNew())
						{
							resetStmt(createBindingStmt);
							createBindingStmt.bind(1, binding.getId());
							createBindingStmt.bind(2, ref.getId().longValue());
							applyStmt(createBindingStmt);
						}
						else
						{
							resetStmt(updateBindingStmt);
							updateBindingStmt.bind(2, binding.getId());
							updateBindingStmt.bind(1, ref.getId().longValue());
							applyStmt(updateBindingStmt);
						}
					}
					binding.set(ref);
					binding.getLock().unlock();
				}
				
				for (ManagedTask e : convertedTasks)
				{
					taskCache.add(e);
					throw new UnsupportedOperationException();
				}
				db.exec("COMMIT", callback);
			}
			catch (Exception e)
			{
				e.printStackTrace();
				System.exit(1);
			}
		}
	}
}

public void revert(Transaction transaction)
{
	for (ManagedEntry entry : transaction.lockedEntries.values())
		entry.getLock().unlock();
	for (Binding binding : transaction.lockedBindings.values())
		binding.getLock().unlock();
}

/*public long getCurrentTime()
{
	synchronized (timeLock)
	{ return currentTime; }
}

public void advanceTime(long totime)
{
	while (true)
	{
		TaskHandle firstHandle;
		ManagedTask firstTask;
		synchronized (cacheLock)
		{
			if (tasks.size() == 0) return;
			Entry<TaskHandle,ManagedTask> firstEntry = tasks.firstEntry();
			firstHandle = firstEntry.getKey();
			firstTask = firstEntry.getValue();
			long taskTime = firstHandle.getTime();
			if (taskTime >= totime) return;
			synchronized (timeLock)
			{ currentTime = taskTime; }
		}
		if (!firstTask.getLock().tryLock())
			return;
		Transaction transaction = new Transaction(this, this.getCurrentTime());
		ManagerContext context = new ManagerContext(transaction);
		AppContextDispatcher.call(context, firstTask);
		synchronized (cacheLock)
		{
			tasks.remove(firstHandle);
		}
	}
}*/

public long getCurrentTime()
{ return System.currentTimeMillis(); }

public void advanceTime(long totime)
{ throw new UnsupportedOperationException(); }

}
